#region Usings

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using log4net;
using Newtonsoft.Json;
using Ninject.Core;
using Ninject.Core.Infrastructure;
using Karma.Framework.Core.Domain;
using Karma.Framework.Core.IoC;
using Karma.Framework.Core.Serialization;
using Karma.Framework.Core.Serialization.Util;
using Karma.Framework.Core.Services.Invoker;
using Karma.Framework.Core.Services.Invoker.Processor;
using Karma.Framework.Core.Utils;
using InvokerConfiguration =
    Karma.Framework.Core.Services.Configuration.ServiceInvokerConfiguration;
using SerializationConfiguration = Karma.Framework.Core.Serialization.Configuration;

#endregion

namespace Karma.Framework.Core.Services.Web
{
    [Singleton]
    public class HttpServiceInvoker : IHttpServiceInvoker
    {
        private static IDictionary<string, JsonConverter[]> DeserializationConverters =
            new Dictionary<string, JsonConverter[]>();

        private static ILog LOG = LogManager.GetLogger(typeof (HttpServiceInvoker).Name);

        private static IDictionary<string, JsonConverter[]> SerializationConverters =
            new Dictionary<string, JsonConverter[]>();

        protected static JsonSerializerSettings settings = new JsonSerializerSettings
                                                               {
                                                                   NullValueHandling = NullValueHandling.Include,
                                                                   ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                                                                   Converters =
                                                                       SerializationConfiguration.EnumConverters
                                                               };

        ///<summary>
        /// Default constructor
        ///</summary>
        ///<param name="invoker"></param>
        public HttpServiceInvoker(IServiceInvoker invoker)
        {
            this.invoker = invoker;
        }

        private IServiceInvoker invoker { get; set; }

        #region IHttpServiceInvoker Members

        public void ProcessRequest(HttpRequest request, HttpResponse response)
        {
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug("Processing request...");
            }
            var reader = new StreamReader(request.InputStream);
            var requestStr = reader.ReadToEnd();

            try
            {
                if (LOG.IsDebugEnabled)
                {
                    LOG.Debug(string.Format("Request string readed: {0}", requestStr));
                }
                Ensure.ArgumentNotNull(requestStr, "Service request string.");
                var serviceRequest = JsonConvert.DeserializeObject<ServiceRequest>(requestStr);

                Ensure.ArgumentNotNull(serviceRequest, "Service Request.");
                if (LOG.IsDebugEnabled)
                {
                    LOG.Debug("Request arguments: ");
                    LOG.Debug(string.Format("Service: {0}", serviceRequest.Service));
                    LOG.Debug(string.Format("Method: {0}", serviceRequest.Method));
                    LOG.Debug(string.Format("Parameters: {0}", serviceRequest.Parameters));
                    LOG.Debug(string.Format("Depth: {0}", serviceRequest.Depth));
                }
                Ensure.ArgumentNotNull(serviceRequest.Service, "Service name.");
                Ensure.ArgumentNotNull(serviceRequest.Method, "Service method.");

                if (serviceRequest.Depth == 0)
                {
                    serviceRequest.Depth = -1;
                }
                else
                {
                    // ignore the depth level of ServiceResponse object
                    serviceRequest.Depth++;
                }

                // invoke service
                if ("Describe".Equals(serviceRequest.Service))
                {
                    ProcessResult(serviceRequest, ProcessDescribeRequest(serviceRequest), GetConverters(),
                                  serviceRequest.Depth, response);
                }
                else
                {
                    try
                    {
                        ProcessResult(serviceRequest, ProcessInvoke(serviceRequest),
                                      GetSerializationConverters(serviceRequest), serviceRequest.Depth,
                                      response);
                    }
                    catch (Exception e)
                    {
                        response.Write(ProcessException(requestStr, e));
                        LOG.Error(string.Format(
                                      "Error invoking, Service [{0}], Method [{1}, Parameters {2}]",
                                      serviceRequest.Service, serviceRequest.Method, serviceRequest.Parameters),
                                  e);
                    }
                }
            }
            catch (Exception e)
            {
                response.Write(ProcessException(requestStr, e));
                response.Flush();
            }
            try
            {
                response.End();
            }
            catch
            {
            }
        }

        #endregion

        private ServiceResponse ProcessDescribeRequest(ServiceRequest serviceRequest)
        {
            ServiceResponse result = null;
            if ("Config".Equals(serviceRequest.Method))
            {
                result = new ServiceResponse(
                    InvokerConfiguration.Instance.Services.Keys.ToArray());
            }
            else
            {
                if ("Enum".Equals(serviceRequest.Method))
                {
                    result = new ServiceResponse(from e in EnumDescriptor.GetEnums().Select(c => c.FullName)
                                                 select new
                                                            {
                                                                Name = e,
                                                                Description = EnumDescriptor.GetDescriptions(e)
                                                     .Select((d, idx) => new {Id = idx, Name = d.Value})
                                                            });
                }
                else
                {
                    LOG.Error(string.Format(
                                  "Operacion no soportada, Servicio [{0}], Metodo [{1}]",
                                  serviceRequest.Service, serviceRequest.Method));
                    throw new InvalidOperationException(
                        string.Format(
                            "Operacion no soportada, Servicio [{0}], Metodo [{1}]",
                            serviceRequest.Service, serviceRequest.Method));
                }
            }
            return result;
        }

        private ServiceResponse ProcessInvoke(ServiceRequest serviceRequest)
        {
            ServiceResponse result;
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("processing parameters: {0}",
                                        serviceRequest.Parameters.Content));
            }
            var parameters = ProcessParameters(serviceRequest);
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug("Parameters processed successfully, now invoke the service.");
            }
            result =
                new ServiceResponse(
                    invoker.Invoke(
                        serviceRequest.Service, serviceRequest.Method,
                        parameters));
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug("Invocation successful!!!");
                LOG.Debug(string.Format("Results from invocation: {0}", result.Result));
            }
            return result;
        }

        private string ProcessException(string requestStr, Exception e)
        {
            e = e.InnerException ?? e;
            var report = !(e.GetType() == typeof (ApplicationException));
            var result = e.InnerException != null
                             ?
                                 new ServiceResponse(
                                     string.Empty, e.InnerException.Message,
                                     e.InnerException.StackTrace, report)
                             : new ServiceResponse(
                                   string.Empty, e.Message, e.StackTrace, report);
            LOG.Error(string.Format("Error invoking, Service [{0}]]", requestStr), e);
            return JsonConvert.SerializeObject(result, Formatting.Indented, settings);
        }

        private void ProcessResult(ServiceRequest serviceRequest, ServiceResponse serviceResponse,
                                   IList<JsonConverter> converters, int depth, HttpResponse response)
        {
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Serializing with depth [{0}].", depth));
            }
            settings.Converters = converters;
            settings.SerializationDepth = depth;
            var jsonresult = JsonConvert.SerializeObject(
                serviceRequest.SerializationType == SerializationType.Wraped
                    ? serviceResponse
                    : serviceResponse.Result, Formatting.Indented, settings);
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Results to be written [{0}].", jsonresult));
            }
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug("Writing the results to the response stream.");
            }
            response.Write(jsonresult);
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug("Results written =).");
            }
            response.Flush();
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug("Results sent to the client =).");
            }
        }

        private static IList<JsonConverter> GetConverters()
        {
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Getting converters from the configuration."));
            }
            var converters = new List<JsonConverter>();
            converters.AddRange(SerializationConfiguration.EnumConverters);
            var conv =
                ExtensionsForIEnumerable.ToList(
                    InvokerConfiguration.Instance.Converters.Values
                        .Select(c => (JsonConverter) Activator.CreateInstance(c)));
            converters.AddRange(conv);
            if (LOG.IsDebugEnabled)
            {
                foreach (var converter in converters)
                {
                    LOG.Debug(string.Format("Converter: {0}", converter.GetType().Name));
                }
            }
            return converters;
        }

        private object[] ProcessParameters(ServiceRequest serviceRequest)
        {
            object[] parameters = null;
            var targetType = InvokerConfiguration.Instance.Services[serviceRequest.Service];

            if (targetType == null)
            {
                throw new ArgumentException("The service {0} is not configured.");
            }
            var parametersArray = GetParameters(serviceRequest.Parameters);
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Parameters array: [{0}] [{1}].", parametersArray.Length,
                                        parametersArray.ItemsToString()));
            }
            var method = ReflectionUtils.TryGetMethodInfo(targetType, serviceRequest.Method, parametersArray.Length);
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Method info: {0}.", method));
            }
            var converters = ProcessorCache.GetDeserializationConverters(targetType, method);
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Converters array: [{0}] [{1}].", converters.Length, converters));
            }
            var parameterTypes = ReflectionUtils.GetParameterTypes(targetType, serviceRequest.Method,
                                                                   parametersArray.Length);
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Parameter types array: [{0}] [{1}].", parameterTypes.Length,
                                        parameterTypes.ItemsToString()));
            }

            // empty parameters
            if (parametersArray.Length == 0)
            {
                parameters = new object[] {};
            }
            else
            {
                if (parameterTypes.Length == 1)
                {
                    parameters = new[]
                                     {
                                         JsonConvert.DeserializeObject(
                                             serviceRequest.Parameters.Content, parameterTypes[0], converters)
                                     };
                }
                else
                {
                    var simple = Enumerable.Where(parameterTypes, p => !(p.IsValueType || p.Equals(typeof (string))))
                                     .Count() == 0;

                    var temp = simple
                                   ?
                                       JsonConvert.DeserializeObject<string[]>(
                                           serviceRequest.Parameters.Content)
                                   :
                                       (from js in JsonConvert.DeserializeObject<JsonRaw[]>(
                                            serviceRequest.Parameters.Content)
                                        select js.Content).ToArray();

                    parameters = new object[temp.Length];
                    for (var idx = 0; idx < temp.Length; idx++)
                    {
                        var paramType = parameterTypes[idx];
                        parameters[idx] = (paramType.IsPrimitive ||
                                           paramType.Equals(typeof (string)))
                                              ? Convert.ChangeType(temp[idx], paramType)
                                              :
                                                  JsonConvert.DeserializeObject(temp[idx], paramType);
                    }
                }
            }

            return parameters;
        }

        private JsonRaw[] GetParameters(JsonRaw parameters)
        {
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Parameters value: {0}", parameters.Content));
            }
            if (string.IsNullOrEmpty(parameters.Content) || parameters.Content.Equals("null"))
            {
                if (LOG.IsDebugEnabled)
                {
                    LOG.Debug("Empty parameters");
                }
                return new JsonRaw[0];
            }
            /* it's an array */
            if (parameters.Content.StartsWith("["))
            {
                if (LOG.IsDebugEnabled)
                {
                    LOG.Debug("Parameters is an array.");
                }
                return JsonConvert.DeserializeObject<JsonRaw[]>(parameters.Content);
            }
            /* it's an object */
            if (parameters.Content.StartsWith("{"))
            {
                if (LOG.IsDebugEnabled)
                {
                    LOG.Debug("Teh parameter is a single object.");
                }
                return new[] {parameters};
            }
            return new[] {parameters};
        }

        private JsonConverter[] GetSerializationConverters(ServiceRequest serviceRequest)
        {
            var key = string.Format("{0}.{1}", serviceRequest.Service, serviceRequest.Method);
            if (SerializationConverters.ContainsKey(key))
            {
                return SerializationConverters[key];
            }
            var converters = new List<JsonConverter>();
            converters.AddRange(SerializationConfiguration.EnumConverters);

            var serviceType = InvokerConfiguration.Instance.Services[serviceRequest.Service];
            var implementationType = IoCUtils.GetImplementationType(serviceType);

            var classConverters = SerializationUtils.GetConverters<SerializationConvertersAttribute>(implementationType);
            if (classConverters != null && classConverters.Length > 0)
            {
                converters.AddRange(classConverters);
            }

            var mi = ReflectionUtils.GetMethod(implementationType, serviceRequest.Method);
            var methodConverters = SerializationUtils.GetConverters<SerializationConvertersAttribute>(mi);
            if (methodConverters != null && methodConverters.Length > 0)
            {
                converters.AddRange(methodConverters);
            }

            SerializationConverters.Add(key, converters.ToArray());
            return SerializationConverters[key];
        }

        private JsonConverter[] GetDeserializationConverters(ServiceRequest serviceRequest)
        {
            var key = string.Format("{0}.{1}", serviceRequest.Service, serviceRequest.Method);
            if (DeserializationConverters.ContainsKey(key))
            {
                return DeserializationConverters[key];
            }
            var serviceType = InvokerConfiguration.Instance.Services[serviceRequest.Service];
            var implementationType = IoCUtils.GetImplementationType(serviceType);
            var mi = ReflectionUtils.GetMethod(implementationType, serviceRequest.Method);
            var converters = SerializationUtils.GetConverters<DeserializationConvertersAttribute>(mi);

            DeserializationConverters.Add(key, converters.ToArray());
            return DeserializationConverters[key];
        }
    }
}